package bsh;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bsh.asm.acessor.FieldObjectAcessor;
import bsh.asm.acessor.ObjectAccessor;
import bsh.asm.acessor.ThisFieldObjectAcessor;
import bsh.compilation.DebugOptions;
import bsh.exception.CompilationAssertionFailed;
import bsh.exception.CompilationFailedException;
import bsh.meta.ArrayTypeWrapper;
import bsh.meta.FieldDescriptor;
import bsh.meta.JavaClassWrapper;
import bsh.meta.MethodDescriptor;
import bsh.meta.MethodScope;
import bsh.meta.Scope;
import bsh.meta.TypeWrapper;
import bsh.utils.reflection.BytecodeHelper;

/**
 * {@link NodeCompiler} para {@link BSHTypedVariableDeclaration}
 * 
 * @author takeshi
 */
public class TypedVariableDeclarationCompiler implements NodeCompiler {

    private final BSHTypedVariableDeclaration node;

    private final CompilationUnit unit;

    /**
     * C'tor
     * 
     * @param node {@link BSHTypedVariableDeclaration}
     * @param unit {@link CompilationUnit}
     */
    public TypedVariableDeclarationCompiler(BSHTypedVariableDeclaration node, CompilationUnit unit) {
        super();
        this.node = node;
        this.unit = unit;
    }

    /**
     * {@inheritDoc}
     */
    public void compile(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        state.addLineNumber(host, this.node.getLineNumber());
        TypeWrapper variableType = this.variableType(state.currentScope());
        if (state.currentScope() instanceof MethodScope && isExecuteMethod(state.currentScope().currentMethod())) {
            // assign variables instead of creating local variables
            for (int i = 1; i < this.node.jjtGetNumChildren(); i++) {
                BSHVariableDeclarator declarator = (BSHVariableDeclarator) this.node.getChild(i);
                this.assignField(variableType, declarator, host, state);
            }
        } else {
            for (int i = 1; i < this.node.jjtGetNumChildren(); i++) {
                BSHVariableDeclarator declarator = (BSHVariableDeclarator) this.node.getChild(i);
                this.compileTypeDeclarator(variableType, declarator, host, state);
            }
        }

    }

    /**
     * Assigns a field
     * 
     * @param variableType the variable type
     * @param declarator the declarator
     * @param host the {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @throws CompilationFailedException e
     */
    private void assignField(TypeWrapper variableType, BSHVariableDeclarator declarator, MethodVisitor host, CompilationState state)
            throws CompilationFailedException {
        FieldDescriptor fd = this.unit.thisType().addDelayedField(variableType, declarator.name, parseModifiers(node));
        ObjectAccessor accessor;
        if (fd == null) {
            accessor = this.unit.getFieldAcessor(declarator.name);
        } else if (fd.isStatic()) {
            accessor = new FieldObjectAcessor(fd);
        } else {
            accessor = new ThisFieldObjectAcessor(unit.thisType(), fd);
        }
        if (declarator.jjtGetNumChildren() > 0) {
            accessor.preStore(host, state);
            this.unit.getNodeCompiler(declarator.getChild(0)).compile(host, state);
            this.unit.getCompilerManager().coerceStackTopTo(host, variableType, state);
            accessor.storeObject(host, state);
        }
        state.currentScope().addAccessor(declarator.name, accessor);
    }

    /**
     * Parses modifiers from a variable declaration
     * 
     * @param node the node
     * @return int
     */
    public static int parseModifiers(BSHTypedVariableDeclaration node) {
        if (node.modifiers == null) {
            return 0;
        }
        int mod = accessModifiers(node.modifiers);
        if (node.modifiers.hasModifier("static")) {
            mod |= Opcodes.ACC_STATIC;
        }
        if (node.modifiers.hasModifier("volatile")) {
            mod |= Opcodes.ACC_VOLATILE;
        }
        if (node.modifiers.hasModifier("strictfp")) {
            mod |= Opcodes.ACC_STRICT;
        }
        return mod;
    }

    /**
     * Parses access modifiers
     * 
     * @param modifiers {@link Modifiers}
     * @return int
     */
    public static int accessModifiers(Modifiers modifiers) {
        if (modifiers.hasModifier("private")) {
            return Opcodes.ACC_PRIVATE;
        } else if (modifiers.hasModifier("protected")) {
            return Opcodes.ACC_PROTECTED;
        } else if (modifiers.hasModifier("public")) {
            return Opcodes.ACC_PUBLIC;
        } else {
            return 0;
        }
    }

    /**
     * Verifies if the given method scope represents the execute method
     * 
     * @param methodScope {@link MethodScope}
     * @return boolean
     */
    private boolean isExecuteMethod(MethodScope methodScope) {
        if (methodScope == null) {
            return false;
        }
        final MethodDescriptor enclosingMethod = methodScope.getMethod();
        return enclosingMethod.methodName().equals("executeScript") && enclosingMethod.parameterTypes().isEmpty();
    }

    /**
     * Compila a declaracao de variavel
     * 
     * @param variableType o tipo da variavel
     * @param declarator o {@link BSHVariableDeclarator}
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @throws CompilationFailedException e
     */
    private void compileTypeDeclarator(TypeWrapper variableType, BSHVariableDeclarator declarator, MethodVisitor host,
            CompilationState state) throws CompilationFailedException {
        String varName = declarator.name;
        LocalVariable localVariable = state.currentScope().getVariable(varName);
        if (localVariable == null) {
            Label start = new Label();
            host.visitLabel(start);
            localVariable = state.currentScope().addLocalVariable(varName, start, variableType);
        }
        if (declarator.jjtGetNumChildren() > 0) {
            // has initializer
            int oldSize = state.getStackSize();
            this.unit.getNodeCompiler(declarator.getChild(0)).compile(host, state);
            state.assertStackSize(oldSize + 1);
            TypeWrapper foundType = state.peekStack();

            this.unit.getCompilerManager().coerceToType(host, foundType, variableType, state);
            foundType = state.peekStack();
            if (!variableType.isSuperTypeOf(foundType)) {
                throw new CompilationAssertionFailed("expected type: " + variableType + " but was: " + foundType);
            }
            host.visitVarInsn(variableType.toASMType().getOpcode(Opcodes.ISTORE), localVariable.getOffset());
            state.popStack();
            setLocalVariable(variableType, host, state, localVariable);
        } else if (this.unit.options().initializeLocalVariables()) {
            // initialize with default value?
            initializeWithDefaultValue(variableType, host, state, varName, localVariable);
        }
    }

    /**
     * Compiles a variable declaration initialized with a default value.
     * 
     * @param variableType the type
     * @param host the visitor
     * @param state the current state
     * @param varName the variable name
     * @param localVariable the localVariable that represents
     * @throws CompilationFailedException e
     */
    public static void initializeWithDefaultValue(TypeWrapper variableType, MethodVisitor host, CompilationState state, String varName,
            LocalVariable localVariable) throws CompilationFailedException {
        if (localVariable.getType().isPrimitive()) {
            if (localVariable.getType().equals(JavaClassWrapper.DOUBLE_WRAPPER)) {
                BytecodeHelper.pushDoubleConst(0, host);
                state.pushStack(double.class);
            } else if (localVariable.getType().equals(JavaClassWrapper.LONG_WRAPPER)) {
                BytecodeHelper.pushLongConst(0, host);
                state.pushStack(long.class);
            } else if (localVariable.getType().equals(JavaClassWrapper.FLOAT_WRAPPER)) {
                BytecodeHelper.pushFloatConst(0, host);
                state.pushStack(float.class);
            } else {
                BytecodeHelper.pushIntConst(0, host);
                state.pushStack(int.class);
            }
        } else {
            host.visitInsn(Opcodes.ACONST_NULL);
            state.pushNull();
        }
        host.visitVarInsn(variableType.toASMType().getOpcode(Opcodes.ISTORE), localVariable.getOffset());
        state.popStack();
        setLocalVariable(variableType, host, state, localVariable);
    }

    /**
     * Seta as variaveis locais no stack frame virtual, se a opcao para fazer isso esta feita
     * 
     * @param variableType {@link TypeWrapper}
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @param localVariable {@link LocalVariable}
     * @throws CompilationFailedException e
     */
    public static void setLocalVariable(TypeWrapper variableType, MethodVisitor host, CompilationState state, LocalVariable localVariable)
            throws CompilationFailedException {
        if (state.getUnit().options().debugOptions().contains(DebugOptions.VIRTUAL_STACK_FRAMES)) {
            int expected = state.getStackSize();
            host.visitVarInsn(Opcodes.ALOAD, 0); // load this
            state.pushStack(state.getUnit().thisType());
            host.visitLdcInsn(localVariable.getName());
            state.pushStack(String.class);
            host.visitVarInsn(variableType.toASMType().getOpcode(Opcodes.ILOAD), localVariable.getOffset());
            state.pushStack(variableType);
            state.getUnit().getCompilerManager().coerceStackTopTo(host, JavaClassWrapper.OBJECT_WRAPPER, state);
            state.getUnit().reflectionUtils().getMethodWithName(CompiledScript.class, "setLocalVariable").writeInvoke(host, state);
            state.assertStackSize(expected);
        }
    }

    /**
     * @return {@link TypeWrapper}
     * @param scope the {@link Scope}
     * @throws CompilationFailedException e
     */
    public TypeWrapper variableType(Scope scope) throws CompilationFailedException {
        final BSHType child = (BSHType) this.node.getChild(0);
        return resolveTypeForNode(child, scope);
    }

    /**
     * Tenta resolver o tipo do noh passado
     * 
     * @param child {@link BSHType}
     * @param scope the {@link Scope}
     * @return {@link TypeWrapper}
     * @throws CompilationFailedException e
     */
    public static TypeWrapper resolveTypeForNode(BSHType child, Scope scope) throws CompilationFailedException {
        SimpleNode rawType = child.getChild(0);
        TypeWrapper wrapper;
        if (rawType instanceof BSHPrimitiveType) {
            wrapper = JavaClassWrapper.wrapperFor(((BSHPrimitiveType) rawType).type);
        } else if (rawType instanceof BSHAmbiguousName) {
            String text = ((BSHAmbiguousName) rawType).text;
            if ("var".equals(text)) {
                return JavaClassWrapper.OBJECT_WRAPPER;
            }
            wrapper = scope.resolveType(text);
            if (wrapper == null) {
                throw new CompilationFailedException(child.getLineNumber(), "Cannot resolve variable type: " + text);
            }
        } else {
            throw new CompilationAssertionFailed("expected either AmbiguousName or PrimitiveType but was: " + rawType);
        }
        for (int i = 0; i < child.getArrayDims(); i++) {
            wrapper = new ArrayTypeWrapper(wrapper);
        }
        return wrapper;

    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper nodeResultType(Scope scope) {
        return JavaClassWrapper.VOID_WRAPPER;
    }

}
